Skip to content

v0.3.0: add view counts, logs, and list --json - #3

Merged
pyeom merged 2 commits into
mainfrom
dev
Aug 4, 2026
Merged

v0.3.0: add view counts, logs, and list --json#3
pyeom merged 2 commits into
mainfrom
dev

Conversation

@pyeom

@pyeom pyeom commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • View counts: Track HTML page loads per deployment, persisted across restarts and --update redeploys. Shown in uptool list output.
  • uptool list --json: Machine-readable output for scripts and LLM automation. Access keys of protected deployments excluded.
  • uptool logs: Read daemon logs directly (supports -n <lines>, -f to follow) without going through uptool status. Works even when daemon is down.
  • Bug fixes: Multi-byte character corruption in large request bodies, incorrect URL in deploy --name output, unprotected WebSocket in protected deployments, invalid HTTP method handling, log truncation at 16 KB boundary, uncaught manifest flush exceptions.
  • Breaking changes: Removed MCP server (uptool mcp) and admin web UI (uptool admin). Use CLI directly.
  • Testing: Expanded test suite with new integration tests, added @vitest/coverage-v8 for coverage reporting, CI workflow now runs typecheck and enforces coverage thresholds.

Testing

  • npm run typecheck — Type checking passes
  • npm run test:coverage — All tests pass with coverage thresholds met
  • uptool list — Shows view counts and last-seen times
  • uptool list --json — Valid JSON array with all fields (no keys in protected deployments)
  • uptool logs — Reads daemon logs, survives rotation with -f
  • Large request bodies with multi-byte characters — No corruption
  • Protected deployments — WebSocket requires same auth as page
  • HEAD requests — No body in response

Summary by CodeRabbit

  • New Features

    • Added deployment view counts and last-seen timestamps to listings.
    • Added machine-readable JSON output for deployment listings.
    • Added uptool logs with line limits and live-follow mode.
    • Added stronger protection for live reload connections.
  • Bug Fixes

    • Improved deployment URLs, request handling, log reading, and large UTF-8 uploads.
    • Fixed encoded file paths for removal and rollback operations.
  • Breaking Changes

    • Removed the admin web interface and MCP command; use CLI alternatives instead.
  • Documentation

    • Updated usage guidance for listing deployments and viewing logs.

- Removed MCP server and admin web UI (breaking changes)
- Fixed multi-byte character corruption, WebSocket auth, HTTP method handling
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@pyeom, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 16 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b7e3ae43-2b3c-40c6-a7e9-75cda2e4d81c

📥 Commits

Reviewing files that changed from the base of the PR and between 7e77ee8 and 945e5a1.

📒 Files selected for processing (15)
  • package.json
  • src/commands/deploy.ts
  • src/commands/logs.ts
  • src/commands/status.ts
  • src/server/ws.ts
  • src/storage/index.ts
  • test/api-client.test.ts
  • test/api.test.ts
  • test/cli.test.ts
  • test/config.test.ts
  • test/global-setup.ts
  • test/helpers.ts
  • test/public.test.ts
  • test/status.test.ts
  • test/ws.test.ts
📝 Walkthrough

Walkthrough

Version 0.3.0 narrows the interface to CLI commands, adds JSON listing and log following, records deployment views, hardens HTTP and WebSocket handling, fixes request and log decoding, and adds broad integration and coverage tests.

Changes

CLI surface and release scope

Layer / File(s) Summary
CLI commands and release metadata
package.json, src/cli.ts, src/commands/*, README.md, CHANGELOG.md, plan-uptool-hardening-observabilidad.html
The CLI now supports JSON listings and log following. It uses the package version and removes admin and MCP commands. Deployment URLs remain stable across updates. Release documentation records the new behavior.
HTTP, authentication, WebSocket, and manifest behavior
src/lib/basic-auth.ts, src/lib/slug.ts, src/server/api.ts, src/server/public.ts, src/server/ws.ts, src/storage/index.ts, src/commands/serve.ts
The server validates methods, supports body-safe UTF-8 decoding, protects HTTP and WebSocket access, records HTML view metrics, maintains WebSocket heartbeats, and persists manifest updates safely.
API client and log-tail correctness
src/lib/api-client.ts, src/commands/status.ts
Token paths use shared configuration logic. Log-tail reads handle partial lines, blank files, chunk offsets, and UTF-8 boundaries.
Test harness, coverage, and integration validation
.github/workflows/ci.yml, .gitignore, vitest.config.ts, test/*
CI now runs typecheck and coverage enforcement. Tests add isolated CLI and daemon helpers, cleanup hooks, and coverage for API, configuration, deployment, public access, storage, status, logs, and WebSockets.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PublicServer
  participant Auth
  participant ManifestStore
  participant WsManager
  Client->>PublicServer: Request deployment content
  PublicServer->>Auth: Validate protected access
  Auth-->>PublicServer: Authorization result
  PublicServer->>ManifestStore: Record successful HTML hit
  Client->>WsManager: Request live-reload upgrade
  WsManager->>Auth: Validate protected WebSocket access
  WsManager-->>Client: Accept or reject upgrade
  WsManager->>WsManager: Ping clients and remove stale sockets
Loading

Possibly related PRs

  • pyeom/uptool#1: Extends the same CLI, API, authentication, storage, status, and manifest functionality.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main user-facing changes: view counts, log viewing, and JSON deployment listing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🧹 Nitpick comments (5)
test/global-setup.ts (1)

19-29: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Scope the sweep so it cannot delete a concurrent run's temp dirs.

The sweep removes every matching directory in the shared OS temp dir. If a second test run executes on the same machine, this teardown deletes directories that the other run still uses. That run then fails for an unrelated reason. Filter by modification time, or by a per-run prefix recorded at setup.

♻️ Proposed change — age-based filter
 export function teardown(): void {
   const tmp = os.tmpdir();
+  const startedAt = Date.now();
   let removed = 0;
   for (const name of fs.readdirSync(tmp)) {
     if (!/^uptool-[a-z0-9-]+-/.test(name)) continue;
+    const full = path.join(tmp, name);
     try {
-      fs.rmSync(path.join(tmp, name), { recursive: true, force: true });
+      // Skip anything touched very recently — likely owned by another run.
+      if (startedAt - fs.statSync(full).mtimeMs < 60_000) continue;
+      fs.rmSync(full, { recursive: true, force: true });
       removed++;
     } catch {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/global-setup.ts` around lines 19 - 29, Update the cleanup loop around
readdirSync in the global teardown to avoid deleting active concurrent-run
directories: inspect each matching entry’s modification time and remove it only
when it exceeds the chosen stale-age threshold. Preserve the existing recursive,
forced removal and best-effort error handling for eligible entries.
test/api.test.ts (1)

409-413: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add force: true to the cleanup removal.

fs.rmSync(tmpDir2, { recursive: true }) throws if the directory is already absent. A throw in finally replaces the original test failure and hides the root cause. Also consider guarding bigStore.flushNow(), which writes to the same directory.

♻️ Proposed change
-      fs.rmSync(tmpDir2, { recursive: true });
+      fs.rmSync(tmpDir2, { recursive: true, force: true });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/api.test.ts` around lines 409 - 413, Update the cleanup in the test’s
finally block to remove tmpDir2 with recursive and force options so an
already-absent directory does not mask the original failure. Also guard
bigStore.flushNow() as needed so cleanup does not write to or recreate the
directory before removal.
test/public.test.ts (1)

516-522: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the stale section header.

The header "Handler throw -> 500, not a dead daemon" sits directly above the "View counting" block. The handler-throw test is at line 568. Move or delete this header so the section markers match the tests below them.

🧹 Proposed fix
-  // -------------------------------------------------------------------------
-  // Handler throw -> 500, not a dead daemon
-  // -------------------------------------------------------------------------
-
   // -------------------------------------------------------------------------
   // View counting (recordHit)
   // -------------------------------------------------------------------------
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/public.test.ts` around lines 516 - 522, Remove the stale “Handler throw
-> 500, not a dead daemon” section header from above the “View counting
(recordHit)” block; keep the existing handler-throw test and view-counting
section marker aligned with their respective tests.
test/storage.test.ts (1)

547-568: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This test asserts a copy of the formula, not the production code.

Lines 563-564 reimplement the padding-aware base64 size estimate inline. The assertions then compare that inline copy against Buffer.from(b64, "base64").length. The test passes even if the estimator inside src/storage/index.ts is wrong, because the production estimator is never called. Export the estimator and call it, or assert the limit behavior through store() with a max_file_size set just above and just below the true decoded size.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/storage.test.ts` around lines 547 - 568, Update the base64 size
estimation test to exercise the production estimator used by storage code
instead of duplicating its formula inline. Export and import that estimator from
the relevant storage module, then call it for each padding case while retaining
the decoded-length assertions; alternatively, test store() with max_file_size
thresholds immediately above and below the true decoded size.
test/ws.test.ts (1)

170-182: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Narrow the fake timers to the heartbeat interval.

vi.useFakeTimers() also fakes setImmediate, while connectLr() waits on real ws I/O that runs in the Node event loop. Fake only setInterval/clearInterval so the constructor captures the heartbeat clock without stalling the socket open.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/ws.test.ts` around lines 170 - 182, Update the timer setup in the
heartbeat test around WsManager construction to fake only setInterval and
clearInterval, leaving setImmediate and other real event-loop APIs available for
connectLr() websocket I/O. Preserve the existing timer advancement and cleanup
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Line 29: Update the test:coverage workflow so the CLI is built from current
source before coverage tests run. Prefer adding the existing build command to
the test:coverage script, or update the CLI setup in test/helpers.ts to invoke
the build before executing dist/cli.js.

In `@src/commands/deploy.ts`:
- Around line 198-208: Update the URL identifier selection around urlSlug in the
deploy command so any --update deployment uses opts.update as the stable
identifier, including when result.slug is populated or name lookup returns no
slug. Preserve the existing opts.name preference for new named deployments and
ensure watchTarget/watchSlug use the same stable identifier.

In `@src/commands/logs.ts`:
- Around line 51-57: Update the appended-log read flow around the read loop and
process.stdout.write to use a persistent streaming UTF-8 decoder instead of
decoding each buffer independently. Preserve decoder state across successive
reads, and reset that decoder whenever the truncation or rotation handling at
line 48 resets the file offset.
- Around line 28-29: Update the logs command around readLogTail so it reads
backward through multiple chunks rather than stopping after one 16 KiB chunk.
Continue until the requested lineCount newline delimiters are collected or the
beginning of the file is reached, while preserving the existing tail output
behavior.

In `@src/commands/status.ts`:
- Around line 93-109: Update readLogTail so a chunk without a newline is not
discarded when it contains the file’s final non-empty line; expand the read
window backward until a newline boundary is found, while preserving removal of
the initial partial line. Add a regression test covering a 17 KiB single-line
log and verify the tail remains truthy and is returned by the logs command.

In `@src/server/public.ts`:
- Around line 95-99: Update the public request handler and its response helpers,
including the no-slug path and sendErrorPage, to receive the request method and
suppress response bodies for every HEAD request. Preserve the existing status
codes and headers, but call res.end without content for HEAD while retaining
HTML bodies for GET.

In `@src/server/ws.ts`:
- Around line 47-57: The WebSocket client map currently uses the host-derived
slug instead of the canonical slug, preventing reload broadcasts from reaching
those clients. In the upgrade flow and the corresponding close and error
handlers, use a shared client key of resolved ?? slug when accessing
this.clients, while preserving the existing authentication and connection
behavior.

In `@src/storage/index.ts`:
- Around line 511-518: Update recordHit so repeated page views do not
continually reset the pending persistence deadline: preserve the hit and
timestamp updates, but ensure scheduleFlush retains an existing timer or
otherwise enforces a maximum flush interval. Use the existing scheduleFlush
mechanism and keep only one pending flush so sustained traffic eventually
persists the changes.

In `@test/api.test.ts`:
- Around line 389-397: Update the response Promise in the affected API test so
the `end` listener rejects with the actual status when it is not 200, rather
than throwing an assertion inside the callback; keep parsing and resolving the
JSON for successful responses, then move the status expectation to execute after
awaiting the Promise.

In `@test/cli.test.ts`:
- Around line 360-366: Strengthen the test case around the deploy invocation by
asserting that runCli(["deploy", f, "--protect", "hunter2"], ...) succeeds
before querying deployments. Update the JSON assertion to verify the protected
entry for the slug created by this test rather than accepting any protected
deployment in the shared daemon.home state, while retaining the access-key
non-leak assertion.

In `@test/config.test.ts`:
- Around line 115-118: The afterEach cleanup unconditionally assigns an
undefined HOME value, causing Node to store the literal "undefined". In
test/config.test.ts lines 115-118 and test/api-client.test.ts lines 25-28,
update the HOME restoration to delete process.env.HOME when the captured value
is undefined; otherwise restore the captured value, then retain the existing
fs.rmSync cleanup.

In `@test/global-setup.ts`:
- Line 20: Update the temp-directory name filter in the global setup cleanup
logic to allow digits in the prefix segment, so names such as uptool-api-utf8-*
are swept while retaining the existing uptool- prefix and trailing-hyphen
requirements.

In `@test/helpers.ts`:
- Around line 188-192: Update startDaemon to continuously drain both
child.stdout and child.stderr with data handlers, retaining only a bounded
recent-output buffer. When waitForPort fails or times out, include the buffered
stdout and stderr tail in the startup error while preserving the existing
successful startup behavior.

In `@test/status.test.ts`:
- Around line 414-441: Replace the fixed-delay settle helper in the followLog
tests with a deadline-based waitFor helper that repeatedly checks for the
expected emitted content and times out clearly; update both append and
truncation tests to use it and raise their test timeout as needed. Keep a fixed
short delay for the stop() releases the watcher absence assertion, or use
waitFor with a rejects assertion.

In `@test/storage.test.ts`:
- Around line 771-787: Update the version-ID generation used by ManifestStore
update/version persistence in src/storage/index.ts so consecutive updates within
the same millisecond always receive distinct IDs; when Date.now() matches an
existing newest version, append or increment a deterministic suffix (or
equivalent unique value). Ensure the generated ID is unique before creating the
version directory and adding it to entry.versions, preserving rollback and
pruning behavior without duplicate manifest entries.

In `@test/ws.test.ts`:
- Around line 62-66: Update the cleanup call in the server.close callback to
pass force: true alongside recursive: true when invoking fs.rmSync on tmpDir,
ensuring cleanup remains idempotent and resolve() is reached if the directory
was already removed.

---

Nitpick comments:
In `@test/api.test.ts`:
- Around line 409-413: Update the cleanup in the test’s finally block to remove
tmpDir2 with recursive and force options so an already-absent directory does not
mask the original failure. Also guard bigStore.flushNow() as needed so cleanup
does not write to or recreate the directory before removal.

In `@test/global-setup.ts`:
- Around line 19-29: Update the cleanup loop around readdirSync in the global
teardown to avoid deleting active concurrent-run directories: inspect each
matching entry’s modification time and remove it only when it exceeds the chosen
stale-age threshold. Preserve the existing recursive, forced removal and
best-effort error handling for eligible entries.

In `@test/public.test.ts`:
- Around line 516-522: Remove the stale “Handler throw -> 500, not a dead
daemon” section header from above the “View counting (recordHit)” block; keep
the existing handler-throw test and view-counting section marker aligned with
their respective tests.

In `@test/storage.test.ts`:
- Around line 547-568: Update the base64 size estimation test to exercise the
production estimator used by storage code instead of duplicating its formula
inline. Export and import that estimator from the relevant storage module, then
call it for each padding case while retaining the decoded-length assertions;
alternatively, test store() with max_file_size thresholds immediately above and
below the true decoded size.

In `@test/ws.test.ts`:
- Around line 170-182: Update the timer setup in the heartbeat test around
WsManager construction to fake only setInterval and clearInterval, leaving
setImmediate and other real event-loop APIs available for connectLr() websocket
I/O. Preserve the existing timer advancement and cleanup behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 284007ca-7ed7-4547-a09b-e369e88a13c0

📥 Commits

Reviewing files that changed from the base of the PR and between 490de9c and 7e77ee8.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (38)
  • .github/workflows/ci.yml
  • .gitignore
  • CHANGELOG.md
  • README.md
  • package.json
  • plan-uptool-hardening-observabilidad.html
  • src/cli.ts
  • src/commands/admin.ts
  • src/commands/deploy.ts
  • src/commands/init.ts
  • src/commands/list.ts
  • src/commands/logs.ts
  • src/commands/mcp.ts
  • src/commands/rm.ts
  • src/commands/rollback.ts
  • src/commands/serve.ts
  • src/commands/status.ts
  • src/lib/api-client.ts
  • src/lib/basic-auth.ts
  • src/lib/slug.ts
  • src/server/admin.ts
  • src/server/api.ts
  • src/server/public.ts
  • src/server/ws.ts
  • src/storage/index.ts
  • test/api-client.test.ts
  • test/api.test.ts
  • test/cli.test.ts
  • test/config.test.ts
  • test/deploy.test.ts
  • test/global-setup.ts
  • test/helpers.test.ts
  • test/helpers.ts
  • test/public.test.ts
  • test/status.test.ts
  • test/storage.test.ts
  • test/ws.test.ts
  • vitest.config.ts
💤 Files with no reviewable changes (3)
  • src/commands/mcp.ts
  • src/server/admin.ts
  • src/commands/admin.ts

Comment thread package.json Outdated
Comment thread src/commands/deploy.ts Outdated
Comment thread src/commands/logs.ts
Comment on lines +28 to +29
const tail = readLogTail(file, lineCount);
if (tail) console.log(tail);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Honor requested line counts above 16 KiB.

Line 28 calls readLogTail, which reads only one 16 KiB chunk. Therefore, uptool logs -n 10000 silently returns only the lines that fit in the final 16 KiB. Read backward in chunks until the requested number of newline delimiters is found or the file start is reached.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/logs.ts` around lines 28 - 29, Update the logs command around
readLogTail so it reads backward through multiple chunks rather than stopping
after one 16 KiB chunk. Continue until the requested lineCount newline
delimiters are collected or the beginning of the file is reached, while
preserving the existing tail output behavior.

Comment thread src/commands/logs.ts Outdated
Comment thread src/commands/status.ts Outdated
Comment thread test/global-setup.ts Outdated
Comment thread test/helpers.ts
Comment thread test/status.test.ts Outdated
Comment thread test/storage.test.ts
Comment thread test/ws.test.ts
- Use canonical slug for deployment URLs and WebSocket broadcasts (fix routing via name)
- StringDecoder for followLog: handles multi-byte UTF-8 split across reads
- readLogTail: widen window for long lines (no newline in initial chunk)
- ManifestStore: keep pending flush timer, don't reset on every hit
- Version IDs: use monotonic timestamp to avoid collisions
- Test: improve daemon error reporting, fix fd/env cleanup, stabilize watchers
@pyeom
pyeom merged commit 7379828 into main Aug 4, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant